Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Raw β€’ Download

feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt c8d9795885f5f4edc710db10bc0d366dd72bccbc (c8d97958) Text, 18.11 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.firmware

Tff7b72import T7ee787org.meshtastic.core.model.DeviceHardware
Tff7b72import T7ee787org.meshtastic.core.model.SoftDeviceVariant
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Tests the pure availability gate and pinned-asset resolvers.
*
* These are the safety-critical decisions for factory erase, so they live in a pure function with no fakes: the
* dangerous direction (writing a wrong-SoftDevice erase image) cannot be tested on hardware without destroying a
* device, which makes table-driven unit coverage of the refusal logic the only real verification available.
*
* Concrete (not a `Common*Test` base) because nothing here touches `CommonUri`, so it needs no platform subclass and
* cannot silently contribute zero tests.
*/
Tff7b72class T56d364UsbMaintenanceGateTest Tb4b4b4{

Tff7b72private Tff7b72fun Td2a8ffnrfTb4b4b4(
Te6edf3variantTb4b4b4: Te6edf3SoftDeviceVariant? Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4,
Te6edf3targetTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Tb4b4b4,
Te6edf3slugTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffRAK4631Ta5d6ff"Tb4b4b4,
Tb4b4b4) Tff7b72= Te6edf3DeviceHardwareTb4b4b4(
Te6edf3hwModelSlug Tff7b72= Te6edf3slugTb4b4b4,
Te6edf3platformioTarget Tff7b72= Te6edf3targetTb4b4b4,
Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffnrf52840Ta5d6ff"Tb4b4b4,
Te6edf3softDeviceVariant Tff7b72= Te6edf3variantTb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffrp2040Tb4b4b4(Te6edf3targetTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffpicoTa5d6ff"Tb4b4b4) Tff7b72=
Te6edf3DeviceHardwareTb4b4b4(Te6edf3hwModelSlug Tff7b72= Ta5d6ff"Ta5d6ffRPI_PICOTa5d6ff"Tb4b4b4, Te6edf3platformioTarget Tff7b72= Te6edf3targetTb4b4b4, Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffrp2040Ta5d6ff"Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffesp32Tb4b4b4(Tb4b4b4) Tff7b72=
Te6edf3DeviceHardwareTb4b4b4(Te6edf3hwModelSlug Tff7b72= Ta5d6ff"Ta5d6ffHELTEC_V3Ta5d6ff"Tb4b4b4, Te6edf3platformioTarget Tff7b72= Ta5d6ff"Ta5d6ffheltec-v3Ta5d6ff"Tb4b4b4, Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffesp32-s3Ta5d6ff"Tb4b4b4)

T8b949e// ── Architecture and transport gating (R11) ──────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is shown for nrf52840 over usb with a release`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showTb4b4b4, Ta5d6ff"Ta5d6ffnRF52840 over USB should offer maintenanceTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4, Ta5d6ff"Ta5d6ffA resolved SoftDevice must not refuseTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is shown for rp2040 over usb and never refuses on softdevice`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showTb4b4b4, Ta5d6ff"Ta5d6ffRP2040 over USB should offer maintenanceTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4, Ta5d6ff"Ta5d6ffRP2040 has no SoftDevice to resolveTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4, Ta5d6ff"Ta5d6ffOTAFIX is nRF-onlyTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is hidden for esp32 even over usb`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertFalseTb4b4b4(Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3esp32Tb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4.Te6edf3showTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is hidden for every non-usb transport`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3listOfTb4b4b4(Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3BleTb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3WifiTb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UnknownTb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3method Tff7b72-Tff7b72>
Te6edf3assertFalseTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3methodTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4.Te6edf3showTb4b4b4,
Ta5d6ff"Ta5d6ffMaintenance must not be offered over Tffd700$Te6edf3methodTa5d6ff β€” the flow needs the UF2 mass-storage driveTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is hidden without a release because there would be nothing to reflash`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertFalseTb4b4b4(Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4.Te6edf3showTb4b4b4)
Tb4b4b4}

T8b949e// ── Fail-closed SoftDevice refusal (R4) ──────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`unresolved softdevice shows the action but refuses it`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showTb4b4b4, Ta5d6ff"Ta5d6ffThe action stays visible so the refusal can be explainedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3UsbMaintenanceRefusalTb4b4b4.Te6edf3UnknownSoftDeviceTb4b4b4, Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`no erase image is resolved for an unresolved softdevice`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3eraseUf2ForTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAn unknown variant must never fall back to a default imageTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`each softdevice variant resolves to its own image and target address`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3six Tff7b72= Te6edf3eraseUf2ForTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3seven Tff7b72= Te6edf3eraseUf2ForTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4)Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3sixTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3sevenTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase2.uf2Ta5d6ff"Tb4b4b4, Te6edf3sixTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase_sd7_3.uf2Ta5d6ff"Tb4b4b4, Te6edf3sevenTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3APP_START_S140_6_1_1Tb4b4b4, Te6edf3sixTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3APP_START_S140_7_3_0Tb4b4b4, Te6edf3sevenTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sixTb4b4b4.Te6edf3sha256 Tff7b72!Tff7b72= Te6edf3sevenTb4b4b4.Te6edf3sha256Tb4b4b4, Ta5d6ff"Ta5d6ffThe two variants must not share a digestTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`esp32 resolves no erase image`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3eraseUf2ForTb4b4b4(Te6edf3esp32Tb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`rp2040 resolves the pico erase image with no address invariant`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3asset Tff7b72= Te6edf3eraseUf2ForTb4b4b4(Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3assetTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffpico_erase.uf2Ta5d6ff"Tb4b4b4, Te6edf3assetTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3assetTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4, Ta5d6ff"Ta5d6ffRP2040 carries no variant-to-address invariantTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e// ── OTAFIX bootloader image resolution (R5) ──────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`otafix is offered only for supported targets`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3target Tff7b72= Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3target Tff7b72= Ta5d6ff"Ta5d6ffheltec-mesh-node-t114Ta5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4,
Ta5d6ff"Ta5d6ffT114 is on OTAFIX's supported list even though the project names differTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`otafix is not offered for products that merely share a supported build target`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// WISMESH Hub/Tap, Nomadstar Meteor Pro and RAK3401 all build against wiscore_rak4631, and T-Echo Plus/Lite
T8b949e// against t-echo, but OTAFIX ships no bootloader for those products.
Te6edf3listOfTb4b4b4(
Ta5d6ff"Ta5d6ffrak2560Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffrak_wismeshtapTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffrak4631_nomadstar_meteor_proTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffrak3401-1wattTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fft-echo-plusTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fft-echo-liteTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3assertFalseTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3nrfTb4b4b4(Te6edf3target Tff7b72= Te6edf3targetTb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4,
Ta5d6ff"Tffd700$Te6edf3targetTa5d6ff is not an OTAFIX-supported productTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// ── Board-ID resolution: the actual safety gate (R5) ─────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`every shipped otafix image resolves and no two boards share a digest or filename`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(T79c0ff1T79c0ff4Tb4b4b4, Te6edf3otafixBoardIdsTb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffOTAFIX 2.2-BP1.3 ships 14 update imagesTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3images Tff7b72= Te6edf3otafixBoardIdsTb4b4b4.Te6edf3map Tb4b4b4{ Te6edf3assertNotNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Tffa657itTb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffno image for Tffd700$Te6edf3itTa5d6ff"Tb4b4b4) Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3imagesTb4b4b4.Te6edf3sizeTb4b4b4, Te6edf3imagesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sha256 Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffNo two boards may share a digestTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3imagesTb4b4b4.Te6edf3sizeTb4b4b4, Te6edf3imagesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fileName Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffNo two boards may share a filenameTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id selects the image rather than the build target`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3rak Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3techo Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Ta5d6ff"Ta5d6ffnRF52840-TEcho-v1Ta5d6ff"Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3rakTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3techoTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3rakTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffwiscore_rak4631_boardTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3techoTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fflilygo_techoTa5d6ff"Tb4b4b4)Tb4b4b4)
T8b949e// Both boards report USB 239A/0029 in bootloader mode, so USB identity could not have told them apart.
Te6edf3assertTrueTb4b4b4(Te6edf3rakTb4b4b4.Te6edf3sha256 Tff7b72!Tff7b72= Te6edf3techoTb4b4b4.Te6edf3sha256Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`xiao sense is distinguished from plain xiao only by board id`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3plain Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Ta5d6ff"Ta5d6ffnRF52840-SeeedXiao-v1Ta5d6ff"Tb4b4b4)
Tff7b72val Te6edf3sense Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Ta5d6ff"Ta5d6ffnRF52840-SeeedXiaoSense-v1Ta5d6ff"Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3plainTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3senseTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3plainTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffxiao_nrf52840_ble_bootloaderTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3senseTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffxiao_nrf52840_ble_senseTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3plainTb4b4b4.Te6edf3sha256 Tff7b72!Tff7b72= Te6edf3senseTb4b4b4.Te6edf3sha256Tb4b4b4, Ta5d6ff"Ta5d6ffOTAFIX's README warns these must not be interchangedTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`an unrecognized board id refuses rather than falling back`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Ta5d6ff"Ta5d6ffSomeOtherBoard-v9Ta5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id is parsed from an INFO_UF2 payload and tolerates surrounding lines`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3info Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.9.2-OTAFIX2.2-BP1.3Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: WisBlock RAK4631 BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: Apr 13 2026Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3infoTb4b4b4)Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3infoTb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3otafixUf2ForBoardIdTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`a payload without a board id line yields null`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Ta5d6ff"Ta5d6ffUF2 Bootloader 0.2.6Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnModel: SomethingTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA volume with no INFO_UF2.TXT is not a UF2 bootloader driveTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id tolerates leading whitespace on its line just like the softdevice line`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Ta5d6ff"Ta5d6ff Board-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e// ── SoftDevice read from the drive: the authoritative gate (R4) ───────────

T8b949e/** Verbatim `INFO_UF2.TXT` from a stock Seeed Wio Tracker L1 (hwModel 99), captured 2026-07-30. */
Tff7b72private Tff7b72val Te6edf3seeedL1Info Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.9.2-dirty lib/nrfx (v2.0.0) lib/tinyusb (0.12.0-145-g9775e7691) Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6fflib/uf2 (remotes/origin/configupdate-9-gadbb8c7)Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: Seeed TRACKER L1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: TRACKER L1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: May 15 2025Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffSoftDevice: S140 7.3.0Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

Tf0883e@Test
Tff7b72fun Td2a8ff`softdevice and board id are both read from a real stock bootloader payload`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffTRACKER L1Ta5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tb4b4b4)
T8b949e// The stock Seeed bootloader reports the same Board-ID as OTAFIX's build for this board.
Te6edf3assertNotNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tff7b72!!Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/** Verbatim `INFO_UF2.TXT` from a RAK4631 running OTAFIX 2.2-BP1.3 (hwModel 9), captured 2026-07-30. */
Tff7b72private Tff7b72val Te6edf3rak4631OtafixInfo Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.9.2-OTAFIX2.2-BP1.3 lib/nrfx (v2.0.0) lib/tinyusb (0.12.0-145-g9775e7691) Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6fflib/uf2 (remotes/origin/configupdate-9-gadbb8c7)Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: WisBlock RAK4631 BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: Apr 13 2026Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffSoftDevice: S140 6.1.1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

T8b949e/**
* Verbatim `INFO_UF2.TXT` from a RAK4631 on a stock 0.4.3 bootloader (May 2023), captured 2026-07-30.
*
* Two things this vintage proves: the `SoftDevice:` line goes back at least this far, so the bundled-map fallback
* is belt-and-braces rather than the common path; and older bootloaders emit an extra `Ver:` line that 0.9.x
* dropped, which the line-scanning parser must tolerate.
*/
Tff7b72private Tff7b72val Te6edf3rak4631StockInfo Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.4.3Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: WisBlock RAK4631 BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: May 20 2023Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffVer: 0.4.3Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffSoftDevice: S140 6.1.1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

Tf0883e@Test
Tff7b72fun Td2a8ff`an old bootloader vintage still reports its softdevice and board id`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3rak4631StockInfoTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631StockInfoTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id is stable across bootloader vintages for the same board`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// A stock 0.4.3 RAK and an OTAFIX 2.2 RAK report the same Board-ID, so the OTAFIX veto resolves correctly on a
T8b949e// device that has never been upgraded β€” the case that decides whether the upgrade is offerable at all.
Te6edf3assertEqualsTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631StockInfoTb4b4b4)Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`both variants are parsed from real captured bootloader payloads`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The two sides of the split, each from hardware: an OTAFIX RAK4631 and a stock Seeed L1.
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(
Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tff7b72!!Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffAn OTAFIX-flashed device must still resolve its own image, so re-running the upgrade is idempotentTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`softdevice line is parsed for both shipped variants`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S140 6.1.1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S140 7.3.0Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`each erase image targets the app start of the softdevice it is linked for`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Verified against hardware 2026-07-30: on a 6.1.1 RAK4631 the app vector table sits at 0x26000
T8b949e// (sp=0x20040000, top of nRF52840 RAM), and 0x27000 is mid-application. On a 7.3.0 device the app starts at
T8b949e// 0x27000, which makes 0x26000 the SoftDevice's last page β€” the direction that corrupts.
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x26000LTb4b4b4, Te6edf3APP_START_S140_6_1_1Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x27000LTb4b4b4, Te6edf3APP_START_S140_7_3_0Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3APP_START_S140_6_1_1Tb4b4b4, Te6edf3eraseUf2ForVariantTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4)Tb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3APP_START_S140_7_3_0Tb4b4b4, Te6edf3eraseUf2ForVariantTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4)Tb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`unsupported softdevice ids and absent lines yield null`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S132 7.3.0Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffS132 is not an nRF52840 SoftDeviceTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S140 9.9.9Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffNo erase image exists for an unknown versionTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffThe bootloader omits the value when no SD is installedTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffBoard-ID: TRACKER L1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAbsent lineTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`the drive outranks the bundled map`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The map says 6.1.1, the device says 7.3.0 with nothing else to go on β€” but they disagree, so refuse.
Tff7b72val Te6edf3conflict Tff7b72=
Te6edf3resolveNrfEraseImageTb4b4b4(
Te6edf3mapped Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4,
Te6edf3reportedFromDrive Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3conflict Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ConflictTb4b4b4, Ta5d6ff"Ta5d6ffDisagreement must refuse, not pick a sideTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`agreement resolves to the reported variant's image`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3resolved Tff7b72=
Te6edf3resolveNrfEraseImageTb4b4b4(
Te6edf3mapped Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4,
Te6edf3reportedFromDrive Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4,
Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resolved Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ResolvedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase_sd7_3.uf2Ta5d6ff"Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3assetTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3APP_START_S140_7_3_0Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3assetTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`an old bootloader with no softdevice line falls back to the bundled map`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3resolved Tff7b72= Te6edf3resolveNrfEraseImageTb4b4b4(Te6edf3mapped Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3reportedFromDrive Tff7b72= Tff7b72nullTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resolved Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ResolvedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase2.uf2Ta5d6ff"Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3assetTb4b4b4.Te6edf3fileNameTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`a drive report rescues an unmapped model`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// THINKNODE_M8 has no firmware variant on master and so no map row; the drive can still answer.
Tff7b72val Te6edf3resolved Tff7b72= Te6edf3resolveNrfEraseImageTb4b4b4(Te6edf3mapped Tff7b72= Tff7b72nullTb4b4b4, Te6edf3reportedFromDrive Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resolved Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ResolvedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3variantTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`neither source resolving means unresolved`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3EraseImageResolutionTb4b4b4.Te6edf3UnresolvedTb4b4b4, Te6edf3resolveNrfEraseImageTb4b4b4(Te6edf3mapped Tff7b72= Tff7b72nullTb4b4b4, Te6edf3reportedFromDrive Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e// ── UF2 header parsing (R8) ──────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`uf2 target address is read from the first block`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3block Tff7b72= Te6edf3ByteArrayTb4b4b4(Te6edf3UF2_BLOCK_BYTESTb4b4b4)
Te6edf3listOfTb4b4b4(T79c0ff0Te6edf3x55Tb4b4b4, T79c0ff0Te6edf3x46Tb4b4b4, T79c0ff0Te6edf3x32Tb4b4b4, T79c0ff0Te6edf3x0ATb4b4b4)Tb4b4b4.Te6edf3forEachIndexed Tb4b4b4{ Te6edf3iTb4b4b4, Te6edf3b Tff7b72-Tff7b72> Te6edf3blockTff7b72[Te6edf3iTff7b72] Tff7b72= Te6edf3bTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSETTff7b72] Tff7b72= T79c0ff0Te6edf3x00
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSET Tff7b72+ T79c0ff1Tff7b72] Tff7b72= T79c0ff0Te6edf3x70
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSET Tff7b72+ T79c0ff2Tff7b72] Tff7b72= T79c0ff0Te6edf3x02
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSET Tff7b72+ T79c0ff3Tff7b72] Tff7b72= T79c0ff0Te6edf3x00

Te6edf3assertEqualsTb4b4b4(Te6edf3APP_START_S140_7_3_0Tb4b4b4, Te6edf3uf2FirstTargetAddressTb4b4b4(Te6edf3blockTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`non-uf2 payloads yield no target address`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3uf2FirstTargetAddressTb4b4b4(Te6edf3ByteArrayTb4b4b4(Te6edf3UF2_BLOCK_BYTESTb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffZeroed bytes carry no UF2 magicTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3uf2FirstTargetAddressTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA short payload cannot hold a UF2 blockTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.25s